import {
  defineAsyncComponent,
  getCurrentInstance,
  h,
  nextTick,
  render,
  createVNode,
  ref,
  watch,
} from "vue";
import type { Component } from "vue";

type RawSlots = {
  [name: string]: unknown;
  $stable?: boolean;
};
export interface IModalOptions {
  modalComponent: Component | any;
  appendTo?: HTMLElement | string;
  slots?: RawSlots;
  [name: string]: unknown;
}

const getAppendToElement = (
  appendTo: IModalOptions["appendTo"]
): HTMLElement => {
  let appendToEL: HTMLElement | null = document.body;
  if (appendTo) {
    if (typeof appendTo === "string") {
      appendToEL = document.querySelector<HTMLElement>(appendTo);
    }
    if (appendTo instanceof HTMLElement) {
      appendToEL = appendTo;
    }
    if (!(appendToEL instanceof HTMLElement)) {
      appendToEL = document.body;
    }
  }
  return appendToEL;
};

function getProvides(instance: any) {
  let provides = instance?.provides || {};
  if (instance.parent) {
    provides = { ...provides, ...getProvides(instance.parent) };
  }
  return provides;
}

export default function useShowModal() {
  const currentInstance = getCurrentInstance() as any;
  const provides = getProvides(currentInstance);

  function showModal(options: IModalOptions) {
    const container = document.createElement("div");
    const isAsync = typeof options.modalComponent === "function";
    const innerRef = ref();

    const modalComponent = isAsync
      ? defineAsyncComponent(options.modalComponent)
      : options.modalComponent;

    const props: Record<string, any> = {};
    for (const key in options) {
      if (!["modalComponent", "appendTo", "slots"].includes(key))
        props[key] = options[key];
    }

    const vNode = createVNode({
      setup() {
        const instance = getCurrentInstance() as any;
        if (instance) {
          instance.provides = { ...instance.provides, ...provides };
        }
      },
      render: () =>
        h(
          modalComponent,
          {
            open: true,
            ...props,
            ref: innerRef,
            "onUpdate:open": () => {
              nextTick(() => {
                close();
              });
            },
          },
          options.slots
        ),
    });

    render(vNode, container);
    getAppendToElement(options.appendTo).appendChild(container);

    function close() {
      render(null, container);
      container.parentNode?.removeChild(container);
    }

    innerRef.value.$close = close;

    if (!isAsync) {
      return innerRef.value;
    } else {
      return new Promise((resolve) => {
        watch(
          innerRef,
          () => {
            resolve(innerRef.value);
          },
          {
            once: true,
          }
        );
      });
    }
  }

  return showModal;
}
